如同以下程式碼範例
type value struct {
mu sync.Mutex
value int
}
var wg sync.WaitGroup
printSum := func(v1, v2 *value) {
defer wg.Done()
v1.mu.Lock() // 1. 這裡我們試圖訪問帶鎖的部分
defer v1.mu.Unlock() // 2. 這裡我們試圖調用defer關鍵字釋放鎖
time.Sleep(2 * time.Second) // 3. 這裡我們添加休眠時間 以造成死鎖
v2.mu.Lock()
defer v2.mu.Unlock()
fmt.Printf("sum=%v\n", v1.value+v2.value)
}
var a, b value
wg.Add(2)
go printSum(&a, &b)
go printSum(&b, &a)
wg.Wait()
如果你運行這段程式,會得到The fatal error: all goroutines are asleep - deadlock!
這段程式意思是啟動了兩個 goroutines。第一個呼叫 printSum(&a, &b),而第二個則呼叫 printSum(&b, &a)。
在 printSum 函數內,首先鎖定了 v1,然後延遲2秒,再鎖定 v2。
當兩個 goroutines 幾乎同時啟動時會發生什麼事?
第一個 goroutine (printSum(&a, &b)) 鎖定了 a 並開始等待2秒。
第二個 goroutine (printSum(&b, &a)) 鎖定了 b 並開始等待2秒。
2秒後,第一個 goroutine 嘗試鎖定 b,但不能,因為 b 已被第二個 goroutine 鎖定。
同時,第二個 goroutine 嘗試鎖定 a,但也不能,因為 a 已被第一個 goroutine 鎖定。
此時,每個 goroutine 都在等待另一個 goroutine 釋放它所需要的鎖。由於兩者都在等待對方,因此形成了死鎖。這是一個典型的循環等待情況,它是造成死鎖的一個主要因素。
科夫曼條件如下:
書中的舉例是:你有沒有在走廊走向另一個人? 她移動到一邊讓你通過,但你也是這樣做的。所以你轉移到另一邊,但她也是這樣做的。想像這會永遠持續下去,這就是活鎖。
程式碼範例:
func main() {
cadence := sync.NewCond(&sync.Mutex{})
go func() {
for range time.Tick(1 * time.Millisecond) {
cadence.Broadcast()
}
}()
takeStep := func() {
cadence.L.Lock()
cadence.Wait()
cadence.L.Unlock()
}
// <1> tryDir 允許一個人嘗試向某個方向移動並返回,無論他們是否成功。每個方向都表示為試圖朝這個方向移動的次數。
tryDir := func(dirName string, dir *int32, out *bytes.Buffer) bool {
fmt.Fprintf(out, " %v", dirName)
// <2> 首先,我們通過將該方向遞增1來朝著某個方向移動。我們將在第3章詳細討論atomic包。現在,你只需要知道這個包的操作是原子操作。
atomic.AddInt32(dir, 1)
// <3> 每個人必須以相同的速度或節奏移動。takeStep模擬所有動作之間的恒定節奏。
takeStep()
if atomic.LoadInt32(dir) == 1 {
fmt.Fprint(out, ". Success!")
return true
}
takeStep()
// <4> 這個人意識到他們不能在這個方向上放棄。我們通過將該方向遞減1來表示這一點。
atomic.AddInt32(dir, -1)
return false
}
var left, right int32
tryLeft := func(out *bytes.Buffer) bool { return tryDir("left", &left, out) }
tryRight := func(out *bytes.Buffer) bool { return tryDir("right", &right, out) }
walk := func(walking *sync.WaitGroup, name string) {
var out bytes.Buffer
defer func() { fmt.Println(out.String()) }()
defer walking.Done()
fmt.Fprintf(&out, "%v is trying to scoot:", name)
// <1> 我對嘗試次數進行了人為限制,以便該程序結束。在一個有活鎖的程序中,可能沒有這種限制,這就是為什麽它是一個現實工作中的問題。
for i := 0; i < 5; i++ {
// <2> 首先,這個人會試圖向左走,如果失敗了,會嘗試向右走。
if tryLeft(&out) || tryRight(&out) {
return
}
}
fmt.Fprintf(&out, "\n%v tosses her hands up in exasperation!", name)
}
// <3> 這個變量為程序提供了等待,直到兩個人都能夠相互通過或放棄。
var peopleInHallway sync.WaitGroup
peopleInHallway.Add(2)
go walk(&peopleInHallway, "Alice")
go walk(&peopleInHallway, "Barbara")
peopleInHallway.Wait()
}
最後會印出
Alice is trying to scoot: left right left right left right left right left right Ali
ce tosses her hands up in exasperation!
Barbara is trying to scoot: left right left right left right left right left right
Barbara tosses her hands up in exasperation!
模擬的情況如下:
Alice 和 Barbara 同時開始嘗試移動。
她們可能同時嘗試向左或向右移動,從而阻礙對方。
如果她們碰巧同時選擇了相同的方向,她們會回到原來的位置並再次嘗試。
由於她們的動作是同步的(由 takeStep 控制),因此存在她們持續選擇相同方向而無法通過的風險。
這段代碼模擬了兩個人在走廊相遇並嘗試避開彼此的場景。兩個人(Alice 和 Barbara)都試圖向左或向右移動以避開對方,但由於它們的移動是在相同的節奏和模式下進行的,所以它們可能會在相同的時間朝相同的方向移動,從而不斷地互相阻礙而無法通過。
這就是活鎖的典型例子。活鎖是一種程序在執行中處於活躍狀態,但卻不能進行有意義的工作的狀態。換句話說,程序並沒有停止或凍結,但由於某種持續的互相阻礙,所以無法前進。
饑餓描述了一種情境,當一個或多個協程由於資源競爭而無法獲得足夠的CPU時間或其他資源來進行工作。
以下這個例子展示了一個貪婪的goroutine和一個知足的goroutine:
func main() {
var wg sync.WaitGroup
var sharedLock sync.Mutex
const runtime = 1 * time.Second
greedyWorker := func() {
defer wg.Done()
var count int
for begin := time.Now(); time.Since(begin) <= runtime; {
sharedLock.Lock()
time.Sleep(3 * time.Nanosecond)
sharedLock.Unlock()
count++
}
fmt.Printf("Greedy worker was able to execute %v work loops\n", count)
}
politeWorker := func() {
defer wg.Done()
var count int
for begin := time.Now(); time.Since(begin) <= runtime; {
sharedLock.Lock()
time.Sleep(1 * time.Nanosecond)
sharedLock.Unlock()
sharedLock.Lock()
time.Sleep(1 * time.Nanosecond)
sharedLock.Unlock()
sharedLock.Lock()
time.Sleep(1 * time.Nanosecond)
sharedLock.Unlock()
count++
}
fmt.Printf("Polite worker was able to execute %v work loops.\n", count)
}
wg.Add(2)
go greedyWorker()
go politeWorker()
wg.Wait()
}
greedyWorker:當獲得 sharedLock 的鎖定時,這個工作協程會執行較長時間(3奈秒)的休眠。它的主要目的是嘗試在每次循環中獲得鎖,然後稍微持有鎖的時間比 politeWorker 長。
politeWorker:這個工作協程每次循環都會三次嘗試獲得鎖,每次都只持有鎖很短的時間(1奈秒)。這意味著它釋放鎖的頻率比 greedyWorker 高,從而給其他協程更多的機會獲得鎖。
在這個例子中,由於 greedyWorker 持有鎖的時間相對較長,它可能會比 politeWorker 獲得更多的工作循環機會。另一方面,politeWorker 儘管釋放鎖的頻率很高,但由於它每次循環都嘗試三次獲得鎖,這可能會使它的效率受到影響,特別是當 greedyWorker 持續地搶占鎖時。
當你運行這段程式碼時,你可能會發現 greedyWorker 能夠完成的工作循環次數比 politeWorker 多。這意味著 politeWorker 可能會遭受饑餓,因為它雖然試圖對其他工作協程 "有禮貌",但在競爭鎖時往往處於不利地位。
這種情境強調了同步和資源管理在多協程環境中的複雜性,以及如何小心設計協程的互動以避免饑餓和其他潛在問題。